package org.example.searcher;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.ansj.domain.Term;
import org.ansj.splitWord.analysis.ToAnalysis;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

// 通过这个类在内存中构造出索引结构
public class Index {

    private static String INDEX_PATH = null;

    static {
        if (Config.isOnline) {
            INDEX_PATH = "/www/wwwroot/doc_searcher_index/";
        }
        else {
            INDEX_PATH = "D:\\java_develop_doucement\\";
        }
    }

    private ObjectMapper objectMapper = new ObjectMapper();

    // 构建两个锁对象
    private Object locker1 = new Object();
    private Object locker2 = new Object();

    // 使用 数组下标 表示 docId 来引用该索引表，用于正排索引
    private ArrayList<DocInfo> forwardIndex = new ArrayList<>();

    // 使用哈希表来表示倒排索引
    // key 关键词   value 表示 一组和这个词相关联的文章(里面存的是docId，后面可通过正排索引获取文章)
    private HashMap<String, ArrayList<Weight>> invertIndex = new HashMap<>();

    // 1. 给定一个docId，在正排索引中，查询文档的详细信息
    public DocInfo getDocInfo(Integer docId) {
        return forwardIndex.get(docId);
    }

    // 2. 给定一个词，在倒排索引中，查询哪些文档和这个词相关联
    public List<Weight> getInverted(String term) {
        return invertIndex.get(term);
    }

    // 3. 往索引中新增一个文档
    public void addDoc(String title, String url, String content) {
        // 新增文档操作，需要同时给正排索引和倒排索引新增信息
        // 构建正排索引
        DocInfo docInfo = buildForward(title, url, content);
        // 构建倒排索引
        buildInverted(docInfo);
    }

    private void buildInverted(DocInfo docInfo) {

        class WordCnt {
            // 表示这个词在标题中出现的次数
            private int titleCount;
            // 表示这个词在正文中出现的次数
            private int contentCount;
        }

        // 用来统计词频
        HashMap<String, WordCnt> wordCntHashMap = new HashMap<>();

        // 1. 针对文档标题进行分词
        List<Term> terms = ToAnalysis.parse(docInfo.getTitle()).getTerms();
        // 2. 遍历分词结果，统计每个词出现的次数
        for (Term term : terms) {
            // 先判定一下在wordCntHashMap中term是否存在
            String word = term.getName();
            WordCnt wordCnt = wordCntHashMap.get(word);
            if (wordCnt == null) {
                WordCnt newWordCnt = new WordCnt();
                newWordCnt.titleCount = 1;
                newWordCnt.contentCount = 0;
                wordCntHashMap.put(word, newWordCnt);
            }
            else {
                wordCnt.titleCount += 1;
            }
        }
        // 3. 针对正文页进行分词
        terms = ToAnalysis.parse(docInfo.getContent()).getTerms();
        // 4. 遍历分词结果，统计每个词出现的次数
        for (Term term : terms) {
            String word = term.getName();
            WordCnt wordCnt = wordCntHashMap.get(word);
            if (wordCnt == null) {
                WordCnt newWordCnt = new WordCnt();
                newWordCnt.titleCount = 0;
                newWordCnt.contentCount = 1;
                wordCntHashMap.put(word, newWordCnt);
            }
            else {
                wordCnt.contentCount += 1;
            }
        }
        // 5. 把上面的结果汇总到一个HashMap里面
        // 最终文档的权重，就设定为词在标题中出现的次数 * 10 + 正文中出现的次数
        // 6. 遍历刚才的HashMap，依次来更新倒排索引中的结构
        for (Map.Entry<String, WordCnt> entry : wordCntHashMap.entrySet()) {

            // 加锁，解决线程安全问题
            synchronized (locker2) {
                // 先根据这里的词去倒排索引中查一查
                // 倒排拉链
                List<Weight> invertedList = invertIndex.get(entry.getKey());
                if (invertedList == null) {
                    // 如果为空，则插入一个新的键值对
                    ArrayList<Weight> newInvertedList = new ArrayList<>();
                    // 把新的文档(当前 DocInfo)，构造成Weight对象，插入进来
                    Weight weight = new Weight();
                    weight.setDocId(docInfo.getDocId());
                    // 权重计算公式：标题中出现的次数 * 10 + 正文中出现的次数
                    weight.setWeight(entry.getValue().titleCount * 10 + entry.getValue().contentCount);
                    newInvertedList.add(weight);
                    invertIndex.put(entry.getKey(), newInvertedList);
                }
                else {
                    // 若非空，则直接把当前文档构造成一个Weight对象，并添加到倒排拉链后面
                    Weight weight = new Weight();
                    weight.setDocId(docInfo.getDocId());
                    weight.setWeight(entry.getValue().titleCount * 10 + entry.getValue().contentCount);
                    invertedList.add(weight);
                }
            }

        }

    }
    private DocInfo buildForward(String title, String url, String content) {
        DocInfo docInfo = new DocInfo();
        docInfo.setTitle(title);
        docInfo.setUrl(url);
        docInfo.setContent(content);
        // 加锁，解决线程安全问题
        synchronized (locker1) {
            docInfo.setDocId(forwardIndex.size());
            forwardIndex.add(docInfo);
        }
        return docInfo;
    }


    // 4. 把内存中的索引结构保存到磁盘中
    public void save() {
        // 使用两个文件，分别保存正排和倒排
        System.out.println("保存索引开始！");
        long begin = System.currentTimeMillis();
        // 先判断一下索引对应的目录是否存在，不存在就创建
        File indexPathFile = new File(INDEX_PATH);
        if (!indexPathFile.exists()) {
            indexPathFile.mkdirs(); // 创建目录
        }
        File forwardIndexFile = new File(INDEX_PATH + "forward.txt");
        File invertedIndexFile = new File(INDEX_PATH + "inverted.txt");
        try {
            // 序列化 (此处会根据JSON格式将数据写入文件)
            objectMapper.writeValue(forwardIndexFile, forwardIndex);
            objectMapper.writeValue(invertedIndexFile, invertIndex);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        long end = System.currentTimeMillis();
        System.out.println("保存索引完成！消耗时间：" + (end - begin) + "ms");
    }

    // 5. 把磁盘中的索引数据加载到内存中
    public void load() {
        System.out.println("加载索引开始！");
        long begin = System.currentTimeMillis();
        // 先设置加载索引的路径
        File forwardIndexPath = new File(INDEX_PATH + "forward.txt");
        File invertedIndexFile = new File(INDEX_PATH + "inverted.txt");
        try {
            // 反序列化 (这里使用了匿名类进行参数传递，让readValue能够按传入的参数类型将导入的数据按照类型格式进行转换，并按对应属性自动赋值)
            forwardIndex = objectMapper.readValue(forwardIndexPath, new TypeReference<ArrayList<DocInfo>>() {});
            invertIndex = objectMapper.readValue(invertedIndexFile, new TypeReference<HashMap<String, ArrayList<Weight>>>() {});
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        long end = System.currentTimeMillis();
        System.out.println("加载索引完成！消耗时间：" + (end - begin) + "ms");

    }

    public static void main(String[] args) {
        Index index = new Index();
        index.load();
        System.out.println("索引加载完成！");
    }
}
